/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.android.exoplayer.extractor.mp4;

import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.util.Ac3Util;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.NalUnitUtil;
import com.google.android.exoplayer.util.ParsableBitArray;
import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.Util;

import android.util.Pair;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/** Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. */
/* package */ final class AtomParsers {

  /**
   * Parses a trak atom (defined in 14496-12).
   *
   * @param trak Atom to parse.
   * @param mvhd Movie header atom, used to get the timescale.
   * @return A {@link Track} instance, or {@code null} if the track's type isn't supported.
   */
  public static Track parseTrak(Atom.ContainerAtom trak, Atom.LeafAtom mvhd) {
    Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia);
    int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data);
    if (trackType != Track.TYPE_soun && trackType != Track.TYPE_vide && trackType != Track.TYPE_text
        && trackType != Track.TYPE_sbtl && trackType != Track.TYPE_subt) {
      return null;
    }

    TkhdData tkhdData = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).data);
    long duration = tkhdData.duration;
    long movieTimescale = parseMvhd(mvhd.data);
    long durationUs;
    if (duration == -1) {
      durationUs = C.UNKNOWN_TIME_US;
    } else {
      durationUs = Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, movieTimescale);
    }
    Atom.ContainerAtom stbl = mdia.getContainerAtomOfType(Atom.TYPE_minf)
        .getContainerAtomOfType(Atom.TYPE_stbl);

    Pair<Long, String> mdhdData = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data);
    StsdData stsdData = parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data, tkhdData.id,
        durationUs, tkhdData.rotationDegrees, mdhdData.second);
    return stsdData.mediaFormat == null ? null
        : new Track(tkhdData.id, trackType, mdhdData.first, durationUs, stsdData.mediaFormat,
            stsdData.trackEncryptionBoxes, stsdData.nalUnitLengthFieldLength);
  }

  /**
   * Parses an stbl atom (defined in 14496-12).
   *
   * @param track Track to which this sample table corresponds.
   * @param stblAtom stbl (sample table) atom to parse.
   * @return Sample table described by the stbl atom.
   */
  public static TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAtom) {
    // Array of sample sizes.
    ParsableByteArray stsz = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz).data;

    // Entries are byte offsets of chunks.
    ParsableByteArray chunkOffsets;
    Atom.LeafAtom chunkOffsetsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stco);
    if (chunkOffsetsAtom == null) {
      chunkOffsetsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_co64);
    }
    chunkOffsets = chunkOffsetsAtom.data;
    // Entries are (chunk number, number of samples per chunk, sample description index).
    ParsableByteArray stsc = stblAtom.getLeafAtomOfType(Atom.TYPE_stsc).data;
    // Entries are (number of samples, timestamp delta between those samples).
    ParsableByteArray stts = stblAtom.getLeafAtomOfType(Atom.TYPE_stts).data;
    // Entries are the indices of samples that are synchronization samples.
    Atom.LeafAtom stssAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stss);
    ParsableByteArray stss = stssAtom != null ? stssAtom.data : null;
    // Entries are (number of samples, timestamp offset).
    Atom.LeafAtom cttsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_ctts);
    ParsableByteArray ctts = cttsAtom != null ? cttsAtom.data : null;

    // Skip full atom.
    stsz.setPosition(Atom.FULL_HEADER_SIZE);
    int fixedSampleSize = stsz.readUnsignedIntToInt();
    int sampleCount = stsz.readUnsignedIntToInt();

    long[] offsets = new long[sampleCount];
    int[] sizes = new int[sampleCount];
    int maximumSize = 0;
    long[] timestamps = new long[sampleCount];
    int[] flags = new int[sampleCount];
    if (sampleCount == 0) {
      return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags);
    }

    // Prepare to read chunk offsets.
    chunkOffsets.setPosition(Atom.FULL_HEADER_SIZE);
    int chunkCount = chunkOffsets.readUnsignedIntToInt();

    stsc.setPosition(Atom.FULL_HEADER_SIZE);
    int remainingSamplesPerChunkChanges = stsc.readUnsignedIntToInt() - 1;
    Assertions.checkState(stsc.readInt() == 1, "stsc first chunk must be 1");
    int samplesPerChunk = stsc.readUnsignedIntToInt();
    stsc.skipBytes(4); // Skip the sample description index.
    int nextSamplesPerChunkChangeChunkIndex = -1;
    if (remainingSamplesPerChunkChanges > 0) {
      // Store the chunk index when the samples-per-chunk will next change.
      nextSamplesPerChunkChangeChunkIndex = stsc.readUnsignedIntToInt() - 1;
    }

    int chunkIndex = 0;
    int remainingSamplesInChunk = samplesPerChunk;

    // Prepare to read sample timestamps.
    stts.setPosition(Atom.FULL_HEADER_SIZE);
    int remainingTimestampDeltaChanges = stts.readUnsignedIntToInt() - 1;
    int remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt();
    int timestampDeltaInTimeUnits = stts.readUnsignedIntToInt();

    // Prepare to read sample timestamp offsets, if ctts is present.
    int remainingSamplesAtTimestampOffset = 0;
    int remainingTimestampOffsetChanges = 0;
    int timestampOffset = 0;
    if (ctts != null) {
      ctts.setPosition(Atom.FULL_HEADER_SIZE);
      remainingTimestampOffsetChanges = ctts.readUnsignedIntToInt() - 1;
      remainingSamplesAtTimestampOffset = ctts.readUnsignedIntToInt();
      // The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers in
      // version 0 ctts boxes, however some streams violate the spec and use signed integers
      // instead. It's safe to always parse sample offsets as signed integers here, because
      // unsigned integers will still be parsed correctly (unless their top bit is set, which
      // is never true in practice because sample offsets are always small).
      timestampOffset = ctts.readInt();
    }

    int nextSynchronizationSampleIndex = -1;
    int remainingSynchronizationSamples = 0;
    if (stss != null) {
      stss.setPosition(Atom.FULL_HEADER_SIZE);
      remainingSynchronizationSamples = stss.readUnsignedIntToInt();
      nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1;
    }

    // Calculate the chunk offsets
    long offsetBytes;
    if (chunkOffsetsAtom.type == Atom.TYPE_stco) {
      offsetBytes = chunkOffsets.readUnsignedInt();
    } else {
      offsetBytes = chunkOffsets.readUnsignedLongToLong();
    }

    long timestampTimeUnits = 0;
    for (int i = 0; i < sampleCount; i++) {
      offsets[i] = offsetBytes;
      sizes[i] = fixedSampleSize == 0 ? stsz.readUnsignedIntToInt() : fixedSampleSize;
      if (sizes[i] > maximumSize) {
        maximumSize = sizes[i];
      }
      timestamps[i] = timestampTimeUnits + timestampOffset;

      // All samples are synchronization samples if the stss is not present.
      flags[i] = stss == null ? C.SAMPLE_FLAG_SYNC : 0;
      if (i == nextSynchronizationSampleIndex) {
        flags[i] = C.SAMPLE_FLAG_SYNC;
        remainingSynchronizationSamples--;
        if (remainingSynchronizationSamples > 0) {
          nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1;
        }
      }

      // Add on the duration of this sample.
      timestampTimeUnits += timestampDeltaInTimeUnits;
      remainingSamplesAtTimestampDelta--;
      if (remainingSamplesAtTimestampDelta == 0 && remainingTimestampDeltaChanges > 0) {
        remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt();
        timestampDeltaInTimeUnits = stts.readUnsignedIntToInt();
        remainingTimestampDeltaChanges--;
      }

      // Add on the timestamp offset if ctts is present.
      if (ctts != null) {
        remainingSamplesAtTimestampOffset--;
        if (remainingSamplesAtTimestampOffset == 0 && remainingTimestampOffsetChanges > 0) {
          remainingSamplesAtTimestampOffset = ctts.readUnsignedIntToInt();
          // Read a signed offset even for version 0 ctts boxes (see comment above).
          timestampOffset = ctts.readInt();
          remainingTimestampOffsetChanges--;
        }
      }

      // If we're at the last sample in this chunk, move to the next chunk.
      remainingSamplesInChunk--;
      if (remainingSamplesInChunk == 0) {
        chunkIndex++;
        if (chunkIndex < chunkCount) {
          if (chunkOffsetsAtom.type == Atom.TYPE_stco) {
            offsetBytes = chunkOffsets.readUnsignedInt();
          } else {
            offsetBytes = chunkOffsets.readUnsignedLongToLong();
          }
        }

        // Change the samples-per-chunk if required.
        if (chunkIndex == nextSamplesPerChunkChangeChunkIndex) {
          samplesPerChunk = stsc.readUnsignedIntToInt();
          stsc.skipBytes(4); // Skip the sample description index.
          remainingSamplesPerChunkChanges--;
          if (remainingSamplesPerChunkChanges > 0) {
            nextSamplesPerChunkChangeChunkIndex = stsc.readUnsignedIntToInt() - 1;
          }
        }

        // Expect samplesPerChunk samples in the following chunk, if it's before the end.
        if (chunkIndex < chunkCount) {
          remainingSamplesInChunk = samplesPerChunk;
        }
      } else {
        // The next sample follows the current one.
        offsetBytes += sizes[i];
      }
    }

    Util.scaleLargeTimestampsInPlace(timestamps, 1000000, track.timescale);

    // Check all the expected samples have been seen.
    Assertions.checkArgument(remainingSynchronizationSamples == 0);
    Assertions.checkArgument(remainingSamplesAtTimestampDelta == 0);
    Assertions.checkArgument(remainingSamplesInChunk == 0);
    Assertions.checkArgument(remainingTimestampDeltaChanges == 0);
    Assertions.checkArgument(remainingTimestampOffsetChanges == 0);
    return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags);
  }

  /**
   * Parses a mvhd atom (defined in 14496-12), returning the timescale for the movie.
   *
   * @param mvhd Contents of the mvhd atom to be parsed.
   * @return Timescale for the movie.
   */
  private static long parseMvhd(ParsableByteArray mvhd) {
    mvhd.setPosition(Atom.HEADER_SIZE);

    int fullAtom = mvhd.readInt();
    int version = Atom.parseFullAtomVersion(fullAtom);

    mvhd.skipBytes(version == 0 ? 8 : 16);

    return mvhd.readUnsignedInt();
  }

  /**
   * Parses a tkhd atom (defined in 14496-12).
   *
   * @return An object containing the parsed data.
   */
  private static TkhdData parseTkhd(ParsableByteArray tkhd) {
    tkhd.setPosition(Atom.HEADER_SIZE);
    int fullAtom = tkhd.readInt();
    int version = Atom.parseFullAtomVersion(fullAtom);

    tkhd.skipBytes(version == 0 ? 8 : 16);
    int trackId = tkhd.readInt();

    tkhd.skipBytes(4);
    boolean durationUnknown = true;
    int durationPosition = tkhd.getPosition();
    int durationByteCount = version == 0 ? 4 : 8;
    for (int i = 0; i < durationByteCount; i++) {
      if (tkhd.data[durationPosition + i] != -1) {
        durationUnknown = false;
        break;
      }
    }
    long duration;
    if (durationUnknown) {
      tkhd.skipBytes(durationByteCount);
      duration = -1;
    } else {
      duration = version == 0 ? tkhd.readUnsignedInt() : tkhd.readUnsignedLongToLong();
    }

    tkhd.skipBytes(16);
    int a00 = tkhd.readInt();
    int a01 = tkhd.readInt();
    tkhd.skipBytes(4);
    int a10 = tkhd.readInt();
    int a11 = tkhd.readInt();

    int rotationDegrees;
    int fixedOne = 65536;
    if (a00 == 0 && a01 == fixedOne && a10 == -fixedOne && a11 == 0) {
      rotationDegrees = 90;
    } else if (a00 == 0 && a01 == -fixedOne && a10 == fixedOne && a11 == 0) {
      rotationDegrees = 270;
    } else if (a00 == -fixedOne && a01 == 0 && a10 == 0 && a11 == -fixedOne) {
      rotationDegrees = 180;
    } else {
      // Only 0, 90, 180 and 270 are supported. Treat anything else as 0.
      rotationDegrees = 0;
    }

    return new TkhdData(trackId, duration, rotationDegrees);
  }

  /**
   * Parses an hdlr atom.
   *
   * @param hdlr The hdlr atom to parse.
   * @return The track type.
   */
  private static int parseHdlr(ParsableByteArray hdlr) {
    hdlr.setPosition(Atom.FULL_HEADER_SIZE + 4);
    return hdlr.readInt();
  }

  /**
   * Parses an mdhd atom (defined in 14496-12).
   *
   * @param mdhd The mdhd atom to parse.
   * @return A pair consisting of the media timescale defined as the number of time units that pass
   *     in one second, and the language code.
   */
  private static Pair<Long, String> parseMdhd(ParsableByteArray mdhd) {
    mdhd.setPosition(Atom.HEADER_SIZE);
    int fullAtom = mdhd.readInt();
    int version = Atom.parseFullAtomVersion(fullAtom);
    mdhd.skipBytes(version == 0 ? 8 : 16);
    long timescale = mdhd.readUnsignedInt();
    mdhd.skipBytes(version == 0 ? 4 : 8);
    int languageCode = mdhd.readUnsignedShort();
    String language = "" + (char) (((languageCode >> 10) & 0x1F) + 0x60)
        + (char) (((languageCode >> 5) & 0x1F) + 0x60)
        + (char) (((languageCode) & 0x1F) + 0x60);
    return Pair.create(timescale, language);
  }

  /**
   * Parses a stsd atom (defined in 14496-12).
   *
   * @param stsd The stsd atom to parse.
   * @param trackId The track's identifier in its container.
   * @param durationUs The duration of the track in microseconds.
   * @param rotationDegrees The rotation of the track in degrees.
   * @param language The language of the track.
   * @return An object containing the parsed data.
   */
  private static StsdData parseStsd(ParsableByteArray stsd, int trackId, long durationUs,
      int rotationDegrees, String language) {
    stsd.setPosition(Atom.FULL_HEADER_SIZE);
    int numberOfEntries = stsd.readInt();
    StsdData out = new StsdData(numberOfEntries);
    for (int i = 0; i < numberOfEntries; i++) {
      int childStartPosition = stsd.getPosition();
      int childAtomSize = stsd.readInt();
      Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive");
      int childAtomType = stsd.readInt();
      if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_avc3
          || childAtomType == Atom.TYPE_encv || childAtomType == Atom.TYPE_mp4v
          || childAtomType == Atom.TYPE_hvc1 || childAtomType == Atom.TYPE_hev1
          || childAtomType == Atom.TYPE_s263) {
        parseVideoSampleEntry(stsd, childStartPosition, childAtomSize, trackId, durationUs,
            rotationDegrees, out, i);
      } else if (childAtomType == Atom.TYPE_mp4a || childAtomType == Atom.TYPE_enca
          || childAtomType == Atom.TYPE_ac_3 || childAtomType == Atom.TYPE_ec_3
          || childAtomType == Atom.TYPE_dtsc || childAtomType == Atom.TYPE_dtse
          || childAtomType == Atom.TYPE_dtsh || childAtomType == Atom.TYPE_dtsl) {
        parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId,
            durationUs, language, out, i);
      } else if (childAtomType == Atom.TYPE_TTML) {
        out.mediaFormat = MediaFormat.createTextFormat(trackId, MimeTypes.APPLICATION_TTML,
            MediaFormat.NO_VALUE, durationUs, language);
      } else if (childAtomType == Atom.TYPE_tx3g) {
        out.mediaFormat = MediaFormat.createTextFormat(trackId, MimeTypes.APPLICATION_TX3G,
            MediaFormat.NO_VALUE, durationUs, language);
      } else if (childAtomType == Atom.TYPE_stpp) {
        out.mediaFormat = MediaFormat.createTextFormat(trackId, MimeTypes.APPLICATION_TTML,
            MediaFormat.NO_VALUE, durationUs, language, 0 /* subsample timing is absolute */);
      }
      stsd.setPosition(childStartPosition + childAtomSize);
    }
    return out;
  }

  private static void parseVideoSampleEntry(ParsableByteArray parent, int position, int size,
      int trackId, long durationUs, int rotationDegrees, StsdData out, int entryIndex) {
    parent.setPosition(position + Atom.HEADER_SIZE);

    parent.skipBytes(24);
    int width = parent.readUnsignedShort();
    int height = parent.readUnsignedShort();
    boolean pixelWidthHeightRatioFromPasp = false;
    float pixelWidthHeightRatio = 1;
    parent.skipBytes(50);

    List<byte[]> initializationData = null;
    int childPosition = parent.getPosition();
    String mimeType = null;
    while (childPosition - position < size) {
      parent.setPosition(childPosition);
      int childStartPosition = parent.getPosition();
      int childAtomSize = parent.readInt();
      if (childAtomSize == 0 && parent.getPosition() - position == size) {
        // Handle optional terminating four zero bytes in MOV files.
        break;
      }
      Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive");
      int childAtomType = parent.readInt();
      if (childAtomType == Atom.TYPE_avcC) {
        Assertions.checkState(mimeType == null);
        mimeType = MimeTypes.VIDEO_H264;
        AvcCData avcCData = parseAvcCFromParent(parent, childStartPosition);
        initializationData = avcCData.initializationData;
        out.nalUnitLengthFieldLength = avcCData.nalUnitLengthFieldLength;
        if (!pixelWidthHeightRatioFromPasp) {
          pixelWidthHeightRatio = avcCData.pixelWidthAspectRatio;
        }
      } else if (childAtomType == Atom.TYPE_hvcC) {
        Assertions.checkState(mimeType == null);
        mimeType = MimeTypes.VIDEO_H265;
        Pair<List<byte[]>, Integer> hvcCData = parseHvcCFromParent(parent, childStartPosition);
        initializationData = hvcCData.first;
        out.nalUnitLengthFieldLength = hvcCData.second;
      } else if (childAtomType == Atom.TYPE_d263) {
        Assertions.checkState(mimeType == null);
        mimeType = MimeTypes.VIDEO_H263;
      } else if (childAtomType == Atom.TYPE_esds) {
        Assertions.checkState(mimeType == null);
        Pair<String, byte[]> mimeTypeAndInitializationData =
            parseEsdsFromParent(parent, childStartPosition);
        mimeType = mimeTypeAndInitializationData.first;
        initializationData = Collections.singletonList(mimeTypeAndInitializationData.second);
      } else if (childAtomType == Atom.TYPE_sinf) {
        out.trackEncryptionBoxes[entryIndex] =
            parseSinfFromParent(parent, childStartPosition, childAtomSize);
      } else if (childAtomType == Atom.TYPE_pasp) {
        pixelWidthHeightRatio = parsePaspFromParent(parent, childStartPosition);
        pixelWidthHeightRatioFromPasp = true;
      }
      childPosition += childAtomSize;
    }

    // If the media type was not recognized, ignore the track.
    if (mimeType == null) {
      return;
    }

    out.mediaFormat = MediaFormat.createVideoFormat(trackId, mimeType, MediaFormat.NO_VALUE,
        MediaFormat.NO_VALUE, durationUs, width, height, initializationData, rotationDegrees,
        pixelWidthHeightRatio);
  }

  private static AvcCData parseAvcCFromParent(ParsableByteArray parent, int position) {
    parent.setPosition(position + Atom.HEADER_SIZE + 4);
    // Start of the AVCDecoderConfigurationRecord (defined in 14496-15)
    int nalUnitLengthFieldLength = (parent.readUnsignedByte() & 0x3) + 1;
    if (nalUnitLengthFieldLength == 3) {
      throw new IllegalStateException();
    }
    List<byte[]> initializationData = new ArrayList<>();
    float pixelWidthAspectRatio = 1;
    int numSequenceParameterSets = parent.readUnsignedByte() & 0x1F;
    for (int j = 0; j < numSequenceParameterSets; j++) {
      initializationData.add(NalUnitUtil.parseChildNalUnit(parent));
    }
    int numPictureParameterSets = parent.readUnsignedByte();
    for (int j = 0; j < numPictureParameterSets; j++) {
      initializationData.add(NalUnitUtil.parseChildNalUnit(parent));
    }

    if (numSequenceParameterSets > 0) {
      // Parse the first sequence parameter set to obtain pixelWidthAspectRatio.
      ParsableBitArray spsDataBitArray = new ParsableBitArray(initializationData.get(0));
      // Skip the NAL header consisting of the nalUnitLengthField and the type (1 byte).
      spsDataBitArray.setPosition(8 * (nalUnitLengthFieldLength + 1));
      pixelWidthAspectRatio = CodecSpecificDataUtil.parseSpsNalUnit(spsDataBitArray)
          .pixelWidthAspectRatio;
    }

    return new AvcCData(initializationData, nalUnitLengthFieldLength, pixelWidthAspectRatio);
  }

  private static Pair<List<byte[]>, Integer> parseHvcCFromParent(ParsableByteArray parent,
      int position) {
    // Skip to the NAL unit length size field.
    parent.setPosition(position + Atom.HEADER_SIZE + 21);
    int lengthSizeMinusOne = parent.readUnsignedByte() & 0x03;

    // Calculate the combined size of all VPS/SPS/PPS bitstreams.
    int numberOfArrays = parent.readUnsignedByte();
    int csdLength = 0;
    int csdStartPosition = parent.getPosition();
    for (int i = 0; i < numberOfArrays; i++) {
      parent.skipBytes(1); // completeness (1), nal_unit_type (7)
      int numberOfNalUnits = parent.readUnsignedShort();
      for (int j = 0; j < numberOfNalUnits; j++) {
        int nalUnitLength = parent.readUnsignedShort();
        csdLength += 4 + nalUnitLength; // Start code and NAL unit.
        parent.skipBytes(nalUnitLength);
      }
    }

    // Concatenate the codec-specific data into a single buffer.
    parent.setPosition(csdStartPosition);
    byte[] buffer = new byte[csdLength];
    int bufferPosition = 0;
    for (int i = 0; i < numberOfArrays; i++) {
      parent.skipBytes(1); // completeness (1), nal_unit_type (7)
      int numberOfNalUnits = parent.readUnsignedShort();
      for (int j = 0; j < numberOfNalUnits; j++) {
        int nalUnitLength = parent.readUnsignedShort();
        System.arraycopy(NalUnitUtil.NAL_START_CODE, 0, buffer, bufferPosition,
            NalUnitUtil.NAL_START_CODE.length);
        bufferPosition += NalUnitUtil.NAL_START_CODE.length;
        System.arraycopy(parent.data, parent.getPosition(), buffer, bufferPosition, nalUnitLength);
        bufferPosition += nalUnitLength;
        parent.skipBytes(nalUnitLength);
      }
    }

    List<byte[]> initializationData = csdLength == 0 ? null : Collections.singletonList(buffer);
    return Pair.create(initializationData, lengthSizeMinusOne + 1);
  }

  private static TrackEncryptionBox parseSinfFromParent(ParsableByteArray parent, int position,
      int size) {
    int childPosition = position + Atom.HEADER_SIZE;

    TrackEncryptionBox trackEncryptionBox = null;
    while (childPosition - position < size) {
      parent.setPosition(childPosition);
      int childAtomSize = parent.readInt();
      int childAtomType = parent.readInt();
      if (childAtomType == Atom.TYPE_frma) {
        parent.readInt(); // dataFormat.
      } else if (childAtomType == Atom.TYPE_schm) {
        parent.skipBytes(4);
        parent.readInt(); // schemeType. Expect cenc
        parent.readInt(); // schemeVersion. Expect 0x00010000
      } else if (childAtomType == Atom.TYPE_schi) {
        trackEncryptionBox = parseSchiFromParent(parent, childPosition, childAtomSize);
      }
      childPosition += childAtomSize;
    }

    return trackEncryptionBox;
  }

  private static float parsePaspFromParent(ParsableByteArray parent, int position) {
    parent.setPosition(position + Atom.HEADER_SIZE);
    int hSpacing = parent.readUnsignedIntToInt();
    int vSpacing = parent.readUnsignedIntToInt();
    return (float) hSpacing / vSpacing;
  }

  private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position,
      int size) {
    int childPosition = position + Atom.HEADER_SIZE;
    while (childPosition - position < size) {
      parent.setPosition(childPosition);
      int childAtomSize = parent.readInt();
      int childAtomType = parent.readInt();
      if (childAtomType == Atom.TYPE_tenc) {
        parent.skipBytes(4);
        int firstInt = parent.readInt();
        boolean defaultIsEncrypted = (firstInt >> 8) == 1;
        int defaultInitVectorSize = firstInt & 0xFF;
        byte[] defaultKeyId = new byte[16];
        parent.readBytes(defaultKeyId, 0, defaultKeyId.length);
        return new TrackEncryptionBox(defaultIsEncrypted, defaultInitVectorSize, defaultKeyId);
      }
      childPosition += childAtomSize;
    }
    return null;
  }

  private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType, int position,
      int size, int trackId, long durationUs, String language, StsdData out, int entryIndex) {
    parent.setPosition(position + Atom.HEADER_SIZE);
    parent.skipBytes(16);
    int channelCount = parent.readUnsignedShort();
    int sampleSize = parent.readUnsignedShort();
    parent.skipBytes(4);
    int sampleRate = parent.readUnsignedFixedPoint1616();

    // If the atom type determines a MIME type, set it immediately.
    String mimeType = null;
    if (atomType == Atom.TYPE_ac_3) {
      mimeType = MimeTypes.AUDIO_AC3;
    } else if (atomType == Atom.TYPE_ec_3) {
      mimeType = MimeTypes.AUDIO_EC3;
    } else if (atomType == Atom.TYPE_dtsc || atomType == Atom.TYPE_dtse) {
      mimeType = MimeTypes.AUDIO_DTS;
    } else if (atomType == Atom.TYPE_dtsh || atomType == Atom.TYPE_dtsl) {
      mimeType = MimeTypes.AUDIO_DTS_HD;
    }

    byte[] initializationData = null;
    int childPosition = parent.getPosition();
    while (childPosition - position < size) {
      parent.setPosition(childPosition);
      int childStartPosition = parent.getPosition();
      int childAtomSize = parent.readInt();
      Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive");
      int childAtomType = parent.readInt();
      if (atomType == Atom.TYPE_mp4a || atomType == Atom.TYPE_enca) {
        if (childAtomType == Atom.TYPE_esds) {
          Pair<String, byte[]> mimeTypeAndInitializationData =
              parseEsdsFromParent(parent, childStartPosition);
          mimeType = mimeTypeAndInitializationData.first;
          initializationData = mimeTypeAndInitializationData.second;
          if (MimeTypes.AUDIO_AAC.equals(mimeType)) {
            // TODO: Do we really need to do this? See [Internal: b/10903778]
            // Update sampleRate and channelCount from the AudioSpecificConfig initialization data.
            Pair<Integer, Integer> audioSpecificConfig =
                CodecSpecificDataUtil.parseAacAudioSpecificConfig(initializationData);
            sampleRate = audioSpecificConfig.first;
            channelCount = audioSpecificConfig.second;
          }
        } else if (childAtomType == Atom.TYPE_sinf) {
          out.trackEncryptionBoxes[entryIndex] = parseSinfFromParent(parent, childStartPosition,
              childAtomSize);
        }
      } else if (atomType == Atom.TYPE_ac_3 && childAtomType == Atom.TYPE_dac3) {
        // TODO: Choose the right AC-3 track based on the contents of dac3/dec3.
        // TODO: Add support for encryption (by setting out.trackEncryptionBoxes).
        parent.setPosition(Atom.HEADER_SIZE + childStartPosition);
        out.mediaFormat = Ac3Util.parseAnnexFAc3Format(parent, trackId, durationUs, language);
        return;
      } else if (atomType == Atom.TYPE_ec_3 && childAtomType == Atom.TYPE_dec3) {
        parent.setPosition(Atom.HEADER_SIZE + childStartPosition);
        out.mediaFormat = Ac3Util.parseAnnexFEAc3Format(parent, trackId, durationUs, language);
        return;
      } else if ((atomType == Atom.TYPE_dtsc || atomType == Atom.TYPE_dtse
          || atomType == Atom.TYPE_dtsh || atomType == Atom.TYPE_dtsl)
          && childAtomType == Atom.TYPE_ddts) {
        out.mediaFormat = MediaFormat.createAudioFormat(trackId, mimeType, MediaFormat.NO_VALUE,
            MediaFormat.NO_VALUE, durationUs, channelCount, sampleRate, null, language);
        return;
      }
      childPosition += childAtomSize;
    }

    // If the media type was not recognized, ignore the track.
    if (mimeType == null) {
      return;
    }

    out.mediaFormat = MediaFormat.createAudioFormat(trackId, mimeType, MediaFormat.NO_VALUE,
        sampleSize, durationUs, channelCount, sampleRate,
        initializationData == null ? null : Collections.singletonList(initializationData),
        language);
  }

  /** Returns codec-specific initialization data contained in an esds box. */
  private static Pair<String, byte[]> parseEsdsFromParent(ParsableByteArray parent, int position) {
    parent.setPosition(position + Atom.HEADER_SIZE + 4);
    // Start of the ES_Descriptor (defined in 14496-1)
    parent.skipBytes(1); // ES_Descriptor tag
    int varIntByte = parent.readUnsignedByte();
    while (varIntByte > 127) {
      varIntByte = parent.readUnsignedByte();
    }
    parent.skipBytes(2); // ES_ID

    int flags = parent.readUnsignedByte();
    if ((flags & 0x80 /* streamDependenceFlag */) != 0) {
      parent.skipBytes(2);
    }
    if ((flags & 0x40 /* URL_Flag */) != 0) {
      parent.skipBytes(parent.readUnsignedShort());
    }
    if ((flags & 0x20 /* OCRstreamFlag */) != 0) {
      parent.skipBytes(2);
    }

    // Start of the DecoderConfigDescriptor (defined in 14496-1)
    parent.skipBytes(1); // DecoderConfigDescriptor tag
    varIntByte = parent.readUnsignedByte();
    while (varIntByte > 127) {
      varIntByte = parent.readUnsignedByte();
    }

    // Set the MIME type based on the object type indication (14496-1 table 5).
    int objectTypeIndication = parent.readUnsignedByte();
    String mimeType;
    switch (objectTypeIndication) {
      case 0x6B:
        mimeType = MimeTypes.AUDIO_MPEG;
        return Pair.create(mimeType, null);
      case 0x20:
        mimeType = MimeTypes.VIDEO_MP4V;
        break;
      case 0x21:
        mimeType = MimeTypes.VIDEO_H264;
        break;
      case 0x23:
        mimeType = MimeTypes.VIDEO_H265;
        break;
      case 0x40:
      case 0x66:
      case 0x67:
      case 0x68:
        mimeType = MimeTypes.AUDIO_AAC;
        break;
      case 0xA5:
        mimeType = MimeTypes.AUDIO_AC3;
        break;
      case 0xA6:
        mimeType = MimeTypes.AUDIO_EC3;
        break;
      case 0xA9:
      case 0xAC:
        mimeType = MimeTypes.AUDIO_DTS;
        return Pair.create(mimeType, null);
      case 0xAA:
      case 0xAB:
        mimeType = MimeTypes.AUDIO_DTS_HD;
        return Pair.create(mimeType, null);
      default:
        mimeType = null;
        break;
    }

    parent.skipBytes(12);

    // Start of the AudioSpecificConfig.
    parent.skipBytes(1); // AudioSpecificConfig tag
    varIntByte = parent.readUnsignedByte();
    int varInt = varIntByte & 0x7F;
    while (varIntByte > 127) {
      varIntByte = parent.readUnsignedByte();
      varInt = varInt << 8;
      varInt |= varIntByte & 0x7F;
    }
    byte[] initializationData = new byte[varInt];
    parent.readBytes(initializationData, 0, varInt);
    return Pair.create(mimeType, initializationData);
  }

  private AtomParsers() {
    // Prevent instantiation.
  }

  /**
   * Holds data parsed from a tkhd atom.
   */
  private static final class TkhdData {

    private final int id;
    private final long duration;
    private final int rotationDegrees;

    public TkhdData(int id, long duration, int rotationDegrees) {
      this.id = id;
      this.duration = duration;
      this.rotationDegrees = rotationDegrees;
    }

  }

  /**
   * Holds data parsed from an stsd atom and its children.
   */
  private static final class StsdData {

    public final TrackEncryptionBox[] trackEncryptionBoxes;

    public MediaFormat mediaFormat;
    public int nalUnitLengthFieldLength;

    public StsdData(int numberOfEntries) {
      trackEncryptionBoxes = new TrackEncryptionBox[numberOfEntries];
      nalUnitLengthFieldLength = -1;
    }

  }

  /**
   * Holds data parsed from an AvcC atom.
   */
  private static final class AvcCData {

    public final List<byte[]> initializationData;
    public final int nalUnitLengthFieldLength;
    public final float pixelWidthAspectRatio;

    public AvcCData(List<byte[]> initializationData, int nalUnitLengthFieldLength,
        float pixelWidthAspectRatio) {
      this.initializationData = initializationData;
      this.nalUnitLengthFieldLength = nalUnitLengthFieldLength;
      this.pixelWidthAspectRatio = pixelWidthAspectRatio;
    }

  }

}
